package lightrain.db;

import java.lang.reflect.ParameterizedType;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import javax.sql.DataSource;

import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;

import lightrain.code.util.Attr;
import lightrain.code.util.NObject;
import lightrain.db.dialect.Dialect;
import lightrain.db.dialect.impl.MySqlDialect;
import lightrain.db.exception.ModelException;
import lightrain.db.util.ModelHandler;
import lightrain.db.util.ModelUtils;


/**
 * @author Yue hanjiang
 * @param <M>
 * @param <Id>
 */
@SuppressWarnings(value={"rawtypes","unchecked"})
public abstract class Model<M extends Model,Id> extends NObject<M>{

	private static final long serialVersionUID = 6248515284363465073L;
	
	protected boolean isIdAuto = true;
	
	protected String idname = "id";
	
	protected String table;
	
	private DB db;
	
	private Dialect dialect;
	
	private M self = (M) this;
	
	private String tableAlias;
	
	private List<String> columnList = new ArrayList<String>();
	
	private List<Object> params = new ArrayList<Object>();
	
	private StringBuilder build = new StringBuilder();
	
	private boolean haswhere = false;
	
	private Map<String, Object> updateFieldMap = new LinkedHashMap<String, Object>();
	
	
	public List<Object> getParams() {
		return params;
	}

	public void setParams(List<Object> params) {
		this.params = params;
	}

	/**
	 * 获取列表
	 * @auther Y.hj
	 * @return
	 */
	public String[] getColumnNames(){
		return new String[0];
	}
	
	public String getTableName(){
		if(StringUtils.isEmpty(table)){
			this.table = ModelUtils.parseClassNameToTableName(getClass());
		}
		return table;
	}
	
	private Class<M> getModelClass(){
		return (Class<M>) getClass();
	}

	
	/**
	 * 获取主键
	 * @auther Y.hj
	 * @return
	 */
	public Id getId(){
		return (Id) get(idname);
	}
	
	public Dialect getDialect() {
		if(dialect == null){
			return new MySqlDialect(self);
		}
		return dialect;
	}

	public M setDialect(Dialect dialect) {
		this.dialect = dialect;
		return self;
	}

	/**
	 * @param table  表名
	 * @param idname 主键名称
	 */
	public Model( String table,String idname) {
		this.idname = idname;
		this.table = table;
	}

	public Model(String table) {
		this.table = table;
	}

	public M table(String...tables){
		for (int i = 0; i < tables.length; i++) {
			if(i > 0){
				this.table +=",";
			}
			this.table+=tables[i];
		}
		return self;
	}
	
	public M table(Class<M> table){
		return table(ModelUtils.parseClassNameToTableName(table));
	}
	
	public M table(Class<M>...tables){
		for (int i = 0; i < tables.length; i++) {
			if(i > 0){
				this.table+=",";
			}
			this.table+=ModelUtils.parseClassNameToTableName(tables[i]);
		}
		return self;
	}
	
	public List<String> getFieldNames(){
		List<String> fieldNames = new ArrayList<String>();
		for (String key : keySet()) {
			fieldNames.add(key);
		}
		return fieldNames;
	}
	
	private  M self(){
		return  self;
	}

	public M column(String...cols){
		for (String col : cols) {
			columnList.add(col);
		}
		return self;
	}
	
	public M column(String name,String alias){
		columnList.add(name+" AS "+alias);
		return self();
	}
	
	
	public Pager<M> pager(int page,int size){
		
		Pager<M> pager = new Pager<M>();
		Integer total = getCountTotal();
		Integer pageTotal =  total % size == 0 ? total / size : (total / size) + 1;
		pager.setPage(page);
		pager.setSize(size);
		pager.setTotal(total);
		pager.setPageTotal(pageTotal);
		pager.setData(select(page, size));
		
		return pager;
	}
	
	public M set(String name,String sqlstr,Object...params){
		updateFieldMap.put(name, sqlstr);
		addParams(params);
		return self;
	}
	
	public Id save(){
		return save(self);
	}
	
	public Id save(M model){
		String sql = dialect.buildInsertSQL(model);
		if(!isIdAuto){
			db.insert(sql.toString(), model.values().toArray());
			return getId();
		}
		return (Id) db.insertIdAuto(sql.toString(), model.values().toArray());
	}
	
	public int saveBatch(final List<M> models){
		String sql = dialect.buildInsertSQL();
		int[] batchUpdate = db.getJdbcTemplate().batchUpdate(sql, new BatchPreparedStatementSetter() {
			public void setValues(PreparedStatement ps, int i) throws SQLException {
					M m = models.get(i);
					int j = 0;
					for (Object value : m.values()) {
						j++;
						ps.setObject(j, value);
					}
			}
			public int getBatchSize() {
				return models.size();
			}
		});
		return batchUpdate.length;
	}
	
	public <T> T saveOrUpdate(){
		if(getId() == null){
			return (T) save();
		}
		return (T)update();
	}
	
	
	public Integer update(){
		List<Object> array = new ArrayList<>();
		for (java.util.Map.Entry<String, Object> en : entrySet()) {
			if(!en.getKey().equals(getIdname())){
				array.add(en.getValue());
			}
		}
		if(!attr(idname).isEmpty()){
			Id id = (Id) get(idname);
			where();
			eq(idname, id);
		}
		String sql = dialect.bulidUpdateSQL();
		array.addAll(params);
		return db.update(sql,array.toArray());
	}
	
	
	
	public int update(Id id){
		set(idname, id);
		return update();
	}
	
	/**
	 * @author yue571041524@qq.com
	 * @param id
	 * @return
	 */
	public int delete(Id id){
		set(idname, id);
		return delete();
	}
	
	
	public M find(){
		List<M> list = select();
		if(list.size() > 0){
			return list.get(0);
		}
		return null;
	}
	
	public List<M> select(){
		List<Map<String,Object>> list = db.queryList(dialect.bulidSelectSQL(columnList.toArray()), params.toArray());
		List<M> array = new ArrayList<M>();
		for (Map<String, Object> m : list) {
			M object = createThisObject();
			object.putAll(m);
			array.add(object);
		}
		return array;
	}
	
	public List<M> select(int page,int pageSize){
		this.build.append(dialect.bulidLimitSelectSQL());
		this.params.add((page - 1)*pageSize);
		this.params.add(pageSize);
		return select();
	}
	
	public List<M> select(int size){
		return select(1, size);
	}
	
	public Integer getCountTotal(){
		System.out.println(dialect.bulidSelectSQL("COUNT(1)"));
		return db.queryUnique(dialect.bulidSelectSQL("COUNT(1)"), Integer.class,this.params.toArray());
	}
	
	public <T> T  unique(Class<T> resultType){
		return db.queryUnique(dialect.bulidSelectSQL(columnList.toArray()), resultType,this.params.toArray());
	}
	
	public M find(Id id){
		where();
		eq(idname, id);
		return find();
	}
	
	public M and(String andstr,Object...params){
		build(" AND ");
		build(andstr);
		addParams(params);
		return self();
	}
	
	public M and(){
		and("");
		return self();
	}
	
	public M andEq(String name,Object value){
		return and(name+" = ? ",value);
	}
	
	public M ne(String name,Object value){
		build(name).build(" <> ").build("?").addParams(value);
		return self();
	}
	
	public M eq(String name,Object value){
		build(" ").build(name).build(" = ").build(" ? ").addParams(value);
		return self();
	}
	
	
	public M andLike(String name,String like){
		 and(name+" LIKE ?", like);
		 return self();
	}
	
	public M andNe(String name,Object value){
		  return (M) and() .ne(name,value);
	}
	
	public M andIn(String name,Object...values){
		and(name+" IN (");
		for (int i = 0; i < values.length; i++) {
			if(i > 0){
				build(",");
			}
			build("?");
		}
		build(")");
		addParams(values);
		return self;
	}
	
	public M andIn(String name,Model<?,?> model){
		and(name+" IN ("+model.getDialect().bulidSelectSQL()+")");
		addParams(model.getParams().toArray());
		return self;
	}
	
	public M andIn(String name,Class<? extends Model<?,?>> modelClasss,ModelHandler<Model<?,?>> handler){
		Model<?,?> model = ModelUtils.newInstance(modelClasss);
		handler.handle(model);
		andIn(name, model);
		return self;
	}
	
	
	public M leftjoin(Class<M> table,String as,String on1,String on2){
		leftjoin(ModelUtils.parseClassNameToTableName(table), as, on1, on2);
		return self();
	}
	
	public M leftjoin(String table,String as,String on1,String on2){
		leftjoin(table, as, on1+" = "+on2);
		return self();
	}
	
	public M leftjoin(Class<?> table,String as,String on){
		leftjoin(ModelUtils.parseClassNameToTableName(table), as, on);
		return self();
	}
	
	public M leftjoin(String table,String as,String on){
		build(" LEFT JOIN ");
		build(table);
		build(" AS ");
		build(as);
		build(" on ");
		build(on);
		return self();
	}
	
	public M rightjoin(Class<M> table,String as,String on1,String on2){
		rightjoin(ModelUtils.parseClassNameToTableName(table), as, on1, on2);
		return self();
	}
	
	public M rightjoin(String table,String as,String on1,String on2){
		rightjoin(table, as, on1+" = "+on2);
		return self();
	}
	
	public M rightjoin(Class<M> table,String as,String on){
		rightjoin(ModelUtils.parseClassNameToTableName(table), as, on);
		return self();
	}
	
	public M rightjoin(String table,String as,String on){
		build(" RIGHT JOIN ");
		build(table);
		build(" AS ");
		build(as);
		build(" ON ");
		build(on);
		return self();
	}

	public M innerjoin(Class<M> table,String as,String on1,String on2){
		innerjoin(ModelUtils.parseClassNameToTableName(table), as, on1, on2);
		return self();
	}
	
	public M innerjoin(String table,String as,String on1,String on2){
		innerjoin(table, as, on1+" = "+on2);
		return self();
	}
	
	public M innerjoin(Class<M> table,String as,String on){
		innerjoin(ModelUtils.parseClassNameToTableName(table), as, on);
		return self();
	}
	
	public M innerjoin(String table,String as,String on){
		build(" RIGHT JOIN ");
		build(table);
		build(" AS ");
		build(as);
		build(" ON ");
		build(on);
		return self();
	}
	
	public M order(String order){
		build(" ORDER BY "+order);
		return self();
	}
	
	public M group(String group){
		build(" GROUP BY "+group);
		return self();
	}
	
	public M where(){
		if(!haswhere){
			build.append(" WHERE ");
			haswhere = true;
		}
		return self();
	}
	
	
	public M where(Consumer<M> consumer){
		where();
		consumer.accept(self);
		return self;
	}
	
	public M build(String term){
		this.build.append(term);
		return self();
	}
	
	public M addParams(Object...params){
		for (int i = 0; i < params.length; i++) {
			this.params.add(params[i]);
		}
		return self();
	}
	
	public M where(String where,Object...params){
		where().build(where).addParams(params);
		return self();
	}
	
	
	/**
	 * 删除
	 * @author yue571041524@qq.com
	 * @return
	 */
	public int delete(){
		if(!attr(idname).isEmpty()){
			Id id = (Id) get(idname);
			where();
			eq(idname, id);
			remove(idname);
		}
		return db.delete(dialect.bulidDeleteSQL(),this.params.toArray());
	}
	
	public M buildQuery(String query){
		return build(query);
	}

	public M buildSubQuery(String sqlQuery){
		return build("("+sqlQuery+")");
	}
	
	public M buildSubQuery(Class<? extends Model<?,?>> modelClass,ModelHandler<Model<?,?>> handler){
		Model<?,?> model = ModelUtils.newInstance(modelClass);
		handler.handle(model);
		return buildSubQuery(model.getDialect().bulidSelectSQL());
	}
	
	public Model() {
		super();		
	}
	
	
	protected M createThisObject(){
	
		try {
			return getModelClass().newInstance();
		} catch (Exception e) {
			throw new ModelException(e);
		} 
	}
	
	public M setDb(JdbcTemplate jdbcTemplate){
		this.db = new DB(jdbcTemplate);
		return self;
	}
	
	
	public M setDb(DataSource dataSource){
		return setDb(new JdbcTemplate(dataSource));
	}

	public String getTableAlias() {
		return tableAlias;
	}

	public M tableAlias(String tableAlias) {
		this.tableAlias = tableAlias;
		return self;
	}

	public StringBuilder getBuilder() {
		return build;
	}

	public boolean isIdAuto() {
		return isIdAuto;
	}

	public void setIdAuto(boolean isIdAuto) {
		this.isIdAuto = isIdAuto;
	}

	public String getIdname() {
		return idname;
	}

	public void setIdname(String idname) {
		this.idname = idname;
	}

	public Map<String, Object> getUpdateFieldMap() {
		return updateFieldMap;
	}
	
	
}
